﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics.Contracts;
using System.Linq;
using FlitBit.Core;

namespace FlitBit.Cache.Config
{
	/// <summary>
	///   Contains configuration for a named cache.
	/// </summary>
	public class NamedCacheConfigElement : ConfigurationElement
	{
		/// <summary>
		///   Default cache depth.
		/// </summary>
		public const int CDefaultCacheDepth = 100;

		/// <summary>
		///   Default lifespan of cache items. Afterwhich items are evicted.
		/// </summary>
		public const string CDefaultCacheItemLifespan = "0.00:10:00.0";

		/// <summary>
		///   Default concurrent eviction factor.
		/// </summary>
		public const double CDefaultConcurrentEvictionFactor = 1.2;

		/// <summary>
		///   Default concurrent eviction workers.
		/// </summary>
		public const int CDefaultConcurrentEvictionWorkers = 2;

		/// <summary>
		///   Default evictions per synchronized worker.
		/// </summary>
		public const int CDefaultEvictionsPerSynchronizedWorker = 10;

		/// <summary>
		///   Default synchronized eviction factor.
		/// </summary>
		public const double CDefaultSynchronizedEvictionFactor = 2.5;

		const string PropertyNameCacheDepth = "cacheDepth";
		const string PropertyNameCacheItemLifespan = "cacheItemLifespan";
		const string PropertyNameConcurrentEvictionFactor = "concurrentEvictionFactor";
		const string PropertyNameContextKeys = "contextKeys";
		const string PropertyNameEvictionsPerSynchronizedWorker = "evictionsPerSynchronizedWorker";
		const string PropertyNameName = "name";
		const string PropertyNameSynchronizedEvictionFactor = "synchronizedEvictionFactor";
		IEnumerable<string> _contextKeys;

		/// <summary>
		///   Gets the cache's depth; after which evictions will be scheduled/processed.
		/// </summary>
		[ConfigurationProperty(PropertyNameCacheDepth, DefaultValue = CDefaultCacheDepth)]
		public int CacheDepth
		{
			get
			{
				Contract.Ensures(Contract.Result<int>() > 0);

				var res = (int) this[PropertyNameCacheDepth];
				return (res > 0) ? res : CDefaultCacheDepth;
			}
			set
			{
				Contract.Requires<ArgumentOutOfRangeException>(value > 0);

				this[PropertyNameCacheDepth] = value;
			}
		}

		/// <summary>
		///   Gets a cache item's lifespan.
		/// </summary>
		[ConfigurationProperty(PropertyNameCacheItemLifespan, DefaultValue = CDefaultCacheItemLifespan)]
		public string CacheItemLifespan
		{
			get
			{
				Contract.Ensures(Contract.Result<string>() != null);

				return (string) this[PropertyNameCacheItemLifespan];
			}
			set
			{
				TimeSpan it;
				Contract.Requires<ArgumentNullException>(value != null);
				Contract.Requires<ArgumentOutOfRangeException>(TimeSpan.TryParse(value, out it));

				this[PropertyNameCacheItemLifespan] = value;
			}
		}

		/// <summary>
		///   Factor that determines when concurrent eviction threads are
		///   launched.
		/// </summary>
		/// <remarks>
		///   This factor is combined with a cache's depth in order to
		///   determine the cache threshold. When the threshold is exceeded
		///   concurrent eviction workers will be started to remove the
		///   least-recently-used items from the cache.
		/// </remarks>
		[ConfigurationProperty(PropertyNameConcurrentEvictionFactor, DefaultValue = CDefaultConcurrentEvictionFactor)]
		public double ConcurrentEvictionFactor
		{
			get
			{
				Contract.Ensures(Contract.Result<double>() > 0);
				Contract.Ensures(Contract.Result<double>() <= SynchronizedEvictionFactor);

				var res = (double) this[PropertyNameConcurrentEvictionFactor];
				return (res > 0) ? res : CDefaultConcurrentEvictionFactor;
			}
			set
			{
				Contract.Requires<ArgumentOutOfRangeException>(value > 0);
				Contract.Requires<ArgumentOutOfRangeException>(value <= SynchronizedEvictionFactor);

				this[PropertyNameConcurrentEvictionFactor] = value;
			}
		}

		/// <summary>
		///   Gets the cache's context keys.
		/// </summary>
		[ConfigurationProperty(PropertyNameContextKeys)]
		public string ContextKeys
		{
			get { return (string) this[PropertyNameContextKeys]; }
			set
			{
				_contextKeys = null;
				this[PropertyNameContextKeys] = value;
			}
		}

		/// <summary>
		///   Number of evictions processed per synchronized eviction event.
		/// </summary>
		/// <remarks>
		///   If the synchronization threshold is exceeded then a calling
		///   thread is borrowed to process evictions. This setting
		///   indicates the max number of evictions the borrowed thread
		///   should process before returning the thread to the caller.
		/// </remarks>
		[ConfigurationProperty(PropertyNameEvictionsPerSynchronizedWorker,
			DefaultValue = CDefaultEvictionsPerSynchronizedWorker)]
		public int EvictionsPerSynchronizedWorker
		{
			get
			{
				Contract.Ensures(Contract.Result<int>() > 0);

				var res = (int) this[PropertyNameEvictionsPerSynchronizedWorker];
				return (res > 0) ? res : CDefaultEvictionsPerSynchronizedWorker;
			}
			set
			{
				Contract.Requires<ArgumentOutOfRangeException>(value > 0);
				this[PropertyNameEvictionsPerSynchronizedWorker] = value;
			}
		}

		/// <summary>
		///   The cache's name.
		/// </summary>
		[ConfigurationProperty(PropertyNameName
			, IsKey = true
			, IsRequired = true)]
		public string Name
		{
			get
			{
				Contract.Ensures(Contract.Result<string>() != null);
				return (string) this[PropertyNameName];
			}
			set
			{
				Contract.Requires<ArgumentNullException>(value != null);
				this[PropertyNameName] = value;
			}
		}

		/// <summary>
		///   Factor that determines when evictions become synchronous.
		/// </summary>
		/// <remarks>
		///   This factor is combined with a cache's depth in order to
		///   determine the cache synchronization threshold. When the
		///   threshold is exceeded the caller's thread will be borrowed
		///   to process evictions.
		/// </remarks>
		[ConfigurationProperty(PropertyNameSynchronizedEvictionFactor, DefaultValue = CDefaultSynchronizedEvictionFactor)]
		public double SynchronizedEvictionFactor
		{
			get
			{
				Contract.Ensures(Contract.Result<double>() > 0);

				var res = (double) this[PropertyNameSynchronizedEvictionFactor];
				return (res > 0) ? res : ConcurrentEvictionFactor * 2;
			}
			set
			{
				Contract.Requires<ArgumentOutOfRangeException>(value >= ConcurrentEvictionFactor);

				this[PropertyNameSynchronizedEvictionFactor] = value;
			}
		}

		internal TimeSpan ResolvedCacheItemLifespan
		{
			get
			{
				Contract.Requires<InvalidOperationException>(CacheItemLifespan != null);
				return TimeSpan.Parse(this.CacheItemLifespan);
			}
		}

		internal IEnumerable<string> ResolvedContextKeys(Type targetType)
		{
			if (_contextKeys == null)
			{
				var cnf = ContextKeys ?? String.Empty;
				var keys = new List<string>(from k in cnf.Split(';')
																		select k);
				var targetName = targetType.GetReadableFullName();
				if (!keys.Contains(targetName))
				{
					keys.Insert(0, targetName);
				}
				if (!keys.Contains(this.Name))
				{
					keys.Insert(0, this.Name);
				}
				_contextKeys = keys.ToReadOnly();
			}
			return _contextKeys;
		}

		internal MruCacheOptions ToOptions<T>()
		{
			return new MruCacheOptions
			{
				CacheDepth = this.CacheDepth,
				CacheItemLifespan = ResolvedCacheItemLifespan,
				ConcurrentEvictionFactor = this.ConcurrentEvictionFactor,
				SynchronizedEvictionFactor = this.SynchronizedEvictionFactor,
				EvictionsPerSynchronizedWorker = this.EvictionsPerSynchronizedWorker,
				ContextKeys = ResolvedContextKeys(typeof(T))
			};
		}
	}
}